/*
 * Copyright 2024 The CHANGLEI Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import 'dart:async';
import 'dart:collection';

import 'package:flatterer/src/async_task.dart';
import 'package:flatterer/src/change_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

const _secondaryAnimation = kAlwaysDismissedAnimation;

enum _State {
  idle,
  inserted,
  removing,
}

class _Parameters {
  const _Parameters(
    this.duration,
    this.curve,
    this.immediately,
  );

  final Duration? duration;
  final Curve curve;
  final bool immediately;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is _Parameters &&
          runtimeType == other.runtimeType &&
          duration == other.duration &&
          curve == other.curve &&
          immediately == other.immediately;

  @override
  int get hashCode => duration.hashCode ^ curve.hashCode ^ immediately.hashCode;
}

class _Position {
  const _Position(this.below, this.above);

  final OverlayEntries? below;
  final OverlayEntries? above;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is _Position && runtimeType == other.runtimeType && below == other.below && above == other.above;

  @override
  int get hashCode => below.hashCode ^ above.hashCode;
}

class _OverlayEntry extends OverlayEntry {
  _OverlayEntry({
    required WidgetBuilder builder,
    bool opaque = false,
    bool maintainState = false,
  }) : super(
          builder: builder,
          opaque: opaque,
          maintainState: maintainState,
        );

  Object? _owner;

  @override
  WidgetBuilder get builder {
    assert(_owner != null, 'Can only be used internally in `OverlayEntries`.');
    return super.builder;
  }

  @override
  void remove() {
    assert(_owner == null, 'Can only be used internally in `OverlayEntries`.');
    super.remove();
  }
}

/// 构建有动画的[OverlayEntry]
class OverlayEntryBuilder {
  /// 弹窗内容
  const OverlayEntryBuilder({
    required this.builder,
    this.transitionBuilder,
    this.opaque = false,
    this.maintainState = false,
  });

  /// 构建控件
  final RoutePageBuilder builder;

  /// 构建动画
  final RouteTransitionsBuilder? transitionBuilder;

  /// See [OverlayEntry.opaque]
  final bool opaque;

  /// See [OverlayEntry.maintainState]
  final bool maintainState;

  OverlayEntry _build(Animation<double> animation, Animation<double> secondaryAnimation) {
    return _OverlayEntry(
      opaque: opaque,
      maintainState: maintainState,
      builder: (context) {
        return AnimatedBuilder(
          animation: animation,
          child: builder(context, animation, secondaryAnimation),
          builder: (context, child) {
            return transitionBuilder?.call(context, animation, secondaryAnimation, child!) ?? child!;
          },
        );
      },
    );
  }
}

/// 构建有动画的[OverlayEntry]
class LazyOverlayEntryBuilder extends OverlayEntryBuilder {
  /// 弹窗内容
  LazyOverlayEntryBuilder({
    RoutePageBuilder? builder,
    RouteTransitionsBuilder? transitionBuilder,
    bool opaque = false,
    bool maintainState = false,
  })  : _builder = builder,
        _transitionBuilder = transitionBuilder,
        _opaque = opaque,
        _maintainState = maintainState,
        super(builder: _unimplementedBuilder);

  static Widget _unimplementedBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    throw UnimplementedError('请先设置`builder`');
  }

  OverlayEntry? _overlayEntry;

  void _markNeedsBuild() {
    final overlayEntry = _overlayEntry;
    if (overlayEntry != null && overlayEntry.mounted) {
      overlayEntry.markNeedsBuild();
    }
  }

  RoutePageBuilder? _builder;

  @override
  RoutePageBuilder get builder => _builder ?? super.builder;

  set builder(RoutePageBuilder value) {
    if (_builder == value) {
      return;
    }
    _builder = value;
    _markNeedsBuild();
  }

  RouteTransitionsBuilder? _transitionBuilder;

  @override
  RouteTransitionsBuilder? get transitionBuilder => _transitionBuilder ?? super.transitionBuilder;

  set transitionBuilder(RouteTransitionsBuilder? value) {
    if (_transitionBuilder == value) {
      return;
    }
    _transitionBuilder = value;
    _markNeedsBuild();
  }

  bool _opaque;

  @override
  bool get opaque => _overlayEntry?.opaque ?? _opaque;

  set opaque(bool value) {
    _opaque = value;
    _overlayEntry?.opaque = value;
  }

  bool _maintainState;

  @override
  bool get maintainState => _overlayEntry?.maintainState ?? _maintainState;

  set maintainState(bool value) {
    _maintainState = value;
    _overlayEntry?.maintainState = value;
  }

  @override
  OverlayEntry _build(Animation<double> animation, Animation<double> secondaryAnimation) {
    return _overlayEntry = super._build(animation, secondaryAnimation);
  }
}

/// Created by changlei on 2023/5/9.
///
/// 弹窗
class OverlayEntries extends LazyChangeNotifier with IterableMixin<OverlayEntry> {
  /// 弹窗
  OverlayEntries({
    required OverlayState overlay,
    required Iterable<OverlayEntryBuilder> builders,
    double? value,
    Duration? duration,
    Duration? reverseDuration,
    double lowerBound = 0.0,
    double upperBound = 1.0,
    AnimationBehavior animationBehavior = AnimationBehavior.normal,
  })  : assert(builders.isNotEmpty),
        _overlay = overlay,
        _builders = builders,
        _controller = AnimationController(
          value: value,
          duration: duration,
          reverseDuration: reverseDuration,
          lowerBound: lowerBound,
          upperBound: upperBound,
          animationBehavior: animationBehavior,
          vsync: overlay,
        );

  final OverlayState _overlay;
  final Iterable<OverlayEntryBuilder> _builders;
  final AnimationController _controller;

  late final _entries = _builders.map<OverlayEntry>((e) {
    final animation = _controller.view;
    const secondaryAnimation = _secondaryAnimation;
    return e._build(animation, secondaryAnimation);
  }).toList(growable: false);

  final _removingTask = AsyncTask();
  final _stateMounted = ValueNotifier(false);

  _State _state = _State.idle;
  _Parameters? _insertParameters;
  _Parameters? _removeParameters;
  _Position? _position;
  bool _isDisposed = false;
  bool _isDisposedByOwner = false;

  @override
  Iterator<OverlayEntry> get iterator => _entries.iterator;

  /// [OverlayState.insertAll]
  Future<void> insert({
    Duration? duration,
    Curve curve = Curves.linear,
    OverlayEntries? below,
    OverlayEntries? above,
    bool immediately = false,
  }) async {
    assert(_debugAssertNotDisposed());
    assert(!_isDisposedByOwner);
    assert(_overlay.mounted);
    _removingTask.cancel();
    _removeParameters = null;
    final position = _Position(below, above);
    var isIdle = _state == _State.idle;
    if (!isIdle && _position != position) {
      _removeAllEntries();
      isIdle = true;
    }
    _state = _State.inserted;
    _position = position;
    if (isIdle) {
      _insertAllEntries();
      _overlay.insertAll(
        this,
        below: below?.first,
        above: above?.last,
      );
    } else {
      markNeedsBuild();
    }
    final parameters = _Parameters(duration, curve, immediately);
    if (_insertParameters == parameters) {
      return;
    }
    _insertParameters = parameters;
    if (immediately) {
      _controller.value = _controller.upperBound;
    } else {
      await _controller.animateTo(
        _controller.upperBound,
        duration: duration,
        curve: curve,
      );
    }
  }

  /// [OverlayEntry.remove]
  Future<void> remove({
    Duration? duration,
    Curve curve = Curves.linear,
    bool immediately = false,
  }) async {
    assert(_debugAssertNotDisposed());
    if (_state == _State.idle) {
      return;
    }
    _insertParameters = null;
    final parameters = _Parameters(duration, curve, immediately);
    if (_state == _State.removing && _removeParameters == parameters) {
      markNeedsBuild();
      return;
    }
    _state = _State.removing;
    _removeParameters = parameters;
    final completed = await _removingTask.execute(() async {
      if (immediately) {
        if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
          SchedulerBinding.instance.addPostFrameCallback((duration) {
            if (_state == _State.removing) {
              _controller.reset();
            }
          });
        } else {
          _controller.reset();
        }
      } else {
        await _controller.animateBack(
          _controller.lowerBound,
          duration: duration,
          curve: curve,
        );
      }
    });
    _removeParameters = null;
    if (!completed || _state != _State.removing) {
      return;
    }
    _removeAllEntries();
    _state = _State.idle;
    _position = null;
    if (_isDisposed) {
      _dispose();
    }
  }

  /// [OverlayEntry.mounted]
  bool get mounted => any((element) => element.mounted);

  /// [OverlayEntry.opaque]
  bool get opaque => any((element) => element.opaque);

  set opaque(bool value) {
    assert(_debugAssertNotDisposed());
    assert(!_isDisposedByOwner);
    for (final entry in this) {
      entry.opaque = value;
    }
  }

  /// [OverlayEntry.maintainState]
  bool get maintainState => any((element) => element.maintainState);

  set maintainState(bool value) {
    assert(_debugAssertNotDisposed());
    assert(!_isDisposedByOwner);
    for (final entry in this) {
      entry.maintainState = value;
    }
  }

  /// [OverlayEntry.markNeedsBuild]
  void markNeedsBuild() {
    assert(_debugAssertNotDisposed());
    assert(!_isDisposedByOwner);
    for (final entry in this) {
      entry.markNeedsBuild();
    }
  }

  @override
  void didStartListening() {
    _stateMounted.addListener(notifyListeners);
    for (final value in this) {
      value.addListener(_onStateMountedChanged);
    }
  }

  @override
  void didStopListening() {
    _stateMounted.removeListener(notifyListeners);
    for (final value in this) {
      value.removeListener(_onStateMountedChanged);
    }
  }

  void _onStateMountedChanged() {
    _stateMounted.value = mounted;
  }

  @override
  void dispose() {
    if (_isDisposedByOwner) {
      _isDisposedByOwner = false;
      return;
    }
    assert(_debugAssertNotDisposed());
    assert(_state != _State.inserted, 'An $runtimeType must first remove before dispose is called.');
    _isDisposed = true;
    if (_state == _State.idle) {
      _dispose();
    }
  }

  void _dispose() {
    _removingTask.cancel();
    _stateMounted.dispose();
    _controller.dispose();
    _state = _State.idle;
    _insertParameters = null;
    _removeParameters = null;
    _position = null;
    Timer.run(_disposeAllEntries);
    super.dispose();
  }

  void _disposedByOwner() {
    if (mounted) {
      return;
    }
    _isDisposed = true;
    _isDisposedByOwner = true;
    _removeAllEntries();
    _dispose();
  }

  void _insertAllEntries() {
    for (final value in this) {
      value.removeListener(_disposedByOwner);
      value.addListener(_disposedByOwner);
      if (value is _OverlayEntry) {
        value._owner = this;
      }
    }
  }

  void _removeAllEntries() {
    for (final value in this) {
      if (value is _OverlayEntry) {
        value._owner = null;
      }
      value.removeListener(_disposedByOwner);
      value.remove();
    }
  }

  void _disposeAllEntries() {
    for (final value in this) {
      value.dispose();
    }
  }

  bool _debugAssertNotDisposed() {
    assert(() {
      if (_isDisposed && !_isDisposedByOwner) {
        throw FlutterError(
          'A $runtimeType was used after being disposed.\n'
          'Once you have called dispose() on a $runtimeType, it '
          'can no longer be used.',
        );
      }
      return true;
    }());
    return true;
  }
}
